import traceback
from concurrent.futures import as_completed, wait
from concurrent.futures.thread import ThreadPoolExecutor

import logging
import time
from queue import Queue, Empty
from threading import Event, Thread

import requests

from app.tasks.BackEnd import EndPoint, run_backend, NoReply, NoConnection
from app.tasks.FrontEnd import create_frontend
from app.tasks.LogPusher import PushHandler


class Proxy:
    queue = Queue()
    front_end = None
    backends = []
    active_backend = None
    pool = None  # 执行向backend写入的线程池
    # 执行Proxy主进程、FrontEnd监听进程、FrontEnd读取进程的线程池
    threads = ThreadPoolExecutor(max_workers=5)

    task = None  # Proxy主进程

    listening_server = None
    quit = False

    minute = 0
    requests = 0
    max_in_queue = 0

    base_url = ''

    timeouts = 0 # count the time of 'Empty' exception occurs while reading request from the queue

    def init_thread_pool(self, pool_size):
        self.pool = ThreadPoolExecutor(max_workers=pool_size)

    def write_to_front_end(self, data):
        if self.listening_server:
            self.listening_server.write(data)

    def write_to_backends(self, data):
        self.queue.put(data)

    def init_frontend(self, host_port):
        self.listening_server = create_frontend(self, host_port)
        print('listening_server', self.listening_server)

    def init_backends(self, backends):
        for host, port, desc in backends:
            # backend = '%s:%d' % (host, port)
            ep = EndPoint(self, host, port)
            self.backends.append(ep)

    def run(self):
        try:
            self.listening_server.start()
            while not self.quit:
                try:
                    data = self.queue.get(timeout=0.2)
                    self.timeouts = 0
                    tasks = []
                    for backend in self.backends:
                        size = backend.append_request(data)
                        if size > self.max_in_queue:
                            self.max_in_queue = size
                        running = backend.task_running()
                        # print('running', running, 'size', size)
                        if not running or size==1:
                            # print("submit for", backend.name)
                            task = self.pool.submit(run_backend, backend)
                            tasks.append(task)
                    tasks_in_waiting = len(tasks)
                    if tasks_in_waiting:
                        for task in as_completed(tasks):
                            resp = task.result()
                            if isinstance(resp, bytes):
                                self.write_to_front_end(resp)
                                break
                            if isinstance(resp, NoReply):
                                break
                            tasks_in_waiting -= 1
                            if tasks_in_waiting == 0:
                                if isinstance(resp, NoConnection):
                                    reason = str(resp).strip().replace("\r\n", "")
                                    text = "SERVER_ERROR %s\r\n" % reason
                                    resp = text.encode("utf-8")
                                else:
                                    resp = b"SERVER_ERROR proxy error, no any memcached responses the request\r\n"
                                self.write_to_front_end(resp)
                    else:
                        logging.getLogger("Proxy").critical('没有可用的Memcached服务器', {'backend': 'proxy'})
                        resp = b"SERVER_ERROR proxy error, no valid memcached\r\n"
                        self.write_to_front_end(resp)
                    # statistics
                    self.requests += 1
                except Empty as e:
                    self.timeouts += 1
                    if self.timeouts > 150:
                        # there is no request in recent 30 seconds, so we send version request to all backends to avoid disconnection
                        self.timeouts = 0
                        self.keep_backends_alive()

                minute = int(time.time()/60)
                if minute != self.minute:
                    self.pool.submit(lambda x: requests.post(self.base_url + "running/stat", data=x),
                                     dict(max=self.max_in_queue, cmds=self.requests))
                    self.minute = minute
                    self.max_in_queue = 0
                    self.requests = 0
            print("end execution")
        except:
            traceback.print_exc()

    def keep_backends_alive(self):
        tasks = []
        data = b'version\r\n'
        for backend in self.backends:
            size = backend.append_request_optional(data)
            running = backend.task_running()
            # print('running', running, 'size', size)
            if not running or size == 1:
                # print("submit for", backend.name)
                task = self.pool.submit(run_backend, backend)
                tasks.append(task)
        tasks_in_waiting = len(tasks)
        if tasks_in_waiting:
            for task in as_completed(tasks):
                resp = task.result()
                if isinstance(resp, bytes):
                    break
                if isinstance(resp, NoReply):
                    break
                tasks_in_waiting -= 1
                if tasks_in_waiting == 0:
                    break


    def shutdown(self):
        # stop listening incoming port
        self.listening_server.shutdown()
        print("listening_server shut down")
        # waiting for all tasks
        for ep in self.backends:
            ep.shutdown()
        self.backends.clear()
        try:
            self.pool.shutdown(True)
        except Exception:
            pass
        print("pool shut down")
        # stop main thread
        self.quit = True
        self.join()
        print("main task shut down")

    def stop(self):
        self.threads.submit(lambda x: x.shutdown(), self)

    def start(self):
        self.quit = False
        self.task = self.threads.submit(lambda x: x.run(), self)
        print('start proxy task', self.task)

    def is_alive(self):
        print('is_alive proxy task', self.task)
        if self.task and self.task.running():
            return True
        return False

    def join(self):
        print('join proxy task', self.task)
        wait([self.task])


def run_proxy(proxy, pool_size, frontend, backends, base_url):
    logger = logging.getLogger("Proxy")
    with_push_handler = False
    for hdr in logger.handlers:
        if isinstance(hdr, PushHandler):
            with_push_handler = True
            break
    if not with_push_handler:
        hdr = PushHandler(base_url)
        logger.addHandler(hdr)

        logging.getLogger("requests").setLevel(logging.WARNING)
        logging.getLogger("urllib3").setLevel(logging.WARNING)

    proxy.base_url = base_url
    proxy.init_thread_pool(pool_size)
    proxy.init_backends(backends)
    proxy.init_frontend(frontend)
    proxy.start()
    


if __name__ == '__main__':
    proxy = Proxy()
    run_proxy(proxy, 10, ("127.0.0.1", 11211), [("192.168.19.97", 6000)])
    time.sleep(30)
    proxy.shutdown()
    print('ok')
